package core;


import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import persistence.TransactionManager;
import persistence.dao.IContainerToProductsDAO;
import persistence.dao.IProductsDAO;
import persistence.dto.ProductDTO;



public class ProductManager extends ModelObject
{
	/**
	 * Serialization ID
	 */
	private static final long serialVersionUID = -8661625760083369062L;

	/** This variable holds the only instance of this class. */
	private static ProductManager instance = null;
	
	/** A list of all the products in the entire storage unit */
	private List<Product> products = null;
	
	private List<Product> removedProducts;
	/** A map from the barcode of a product to the product */
	private Map<Barcode, Product> barcodeToProduct = null;
	/** 
	 * Maintains a map from a productContainer to a set of products that
	 * are contained in that storage container.
	 */
	private Map<ProductContainer, Set<Product>> containerToProducts = null;
	/**
	 * Maintains a map from a product to all the storage containers that 
	 * product is contained in.
	 */
	private Map<Product, Set<ProductContainer>> productToContainers = null;
        
        /**
         * pointer to the productDAO singleton
         */
        private IProductsDAO productDAO;
	
	/** Maintains a mapping between a product and the List of items of that type. */
	//private Map<Product, List<Item>> productItems = null;

	/**
	 * Returns the instance of ProductManager.  It first checks to see if one
	 * exists and if it does it returns it.  if it doesn't exist it creates one 
	 * and returns the newly created one.
	 * 
	 * @return Returns the instance of ProductManager. Upon return the object will
	 * 		be instantiated.
	 */
	public static ProductManager getInstance()
	{
		if (instance == null)
		{
			instance = new ProductManager();
		}
		return instance;
	}
	
	public static void destroyInstance()
	{
		instance = null;
	}
	
	/**
	 * Default Constructor.  This is a private constructor to enforce
	 * the singleton design pattern.  There will be one and only one
	 * ProduceManager. After this method executes the product manager
	 * will be setup with empty lists, and maps.
	 */
	private ProductManager()
	{
		//create an empty Set and Map
		products = new LinkedList<Product>();
		barcodeToProduct = new HashMap<Barcode, Product>();
		containerToProducts = new HashMap<ProductContainer, Set<Product>>();
		productToContainers = new HashMap<Product, Set<ProductContainer>>();
		removedProducts = new ArrayList<Product>();
	}
	
	public void Initialize()
	{
        productDAO = CoreFacade.getInstance().getFactory().getProductsDAO();
	}
	
	public Product getRemovedProduct(String barcode)
	{
		for(Product prod : removedProducts)
		{
			if(prod.getBarcodeString().equals(barcode))
				return prod;
		}
		return null;
	}
	
	public void addRemovedProduct(Product p)
	{
		removedProducts.add(p);
	}
	
	/**
	 * This method checks the internal data structure for the product.  The unique
	 * Identifier for the product is it's barcode.  So this method will search for
	 * the barcode of 'prod'. If the barcode is found this method will return false,
	 * because you can only have one of each product in the system. Otherwise it will
	 * return true meaning you can add this item.
	 * 
	 * @param prod The product to check if exists. Must be non-null.
	 * @return Returns true if you can add this product, false if you cannot.
	 */
	public boolean canAdd(Product prod)
	{
		assert prod != null;
		
		return (barcodeToProduct.get(prod.getBarCode()) == null);
	}
	
	/**
	 * This adds a new product to the Product Manager.  To add an item this 
	 * method assumes that 'prod' is not contained within the current list
	 * of products.  If it is then and IllegalArgumentException is thrown.
	 * 
	 * After this method runs the Product will be contained in a list as well
	 * as a new empty list which stores all of the containers this product 
	 * is held in.
	 * 
	 * @param prod The new product to add to the ProductManager.
	 * @return Returns true if the operation was successful. False if it 
	 * 		was not.
	 * @throws IllegalArgumentException -- on failure to insert.
	 */
	public boolean addProduct(Product prod) throws IllegalArgumentException
	{
		assert prod != null;
		
		if (barcodeToProduct.containsKey(prod.getBarCode()) == true)
		{
			//throw new IllegalArgumentException();
			return false;
		}
		
		if (products.add(prod) == false)
		{
			throw new IllegalArgumentException();
		}
		
		barcodeToProduct.put(prod.getBarCode(), prod);
		
		Set<ProductContainer> newSet = new HashSet<ProductContainer>();
		
		productToContainers.put(prod, newSet);
		
		return true;
	}
	
	/**
	 * This method will remove a product from the ProductManager.  This 
	 * will remove the product entirely from the system. This method 
	 * assumes that the product exists and is contained within this
	 * list.  If it is not then this method will return false.
	 * 
	 * After this method executes the ProductManager will not contain
	 * the product in any of its internal storage containers.
	 * 
	 * @param prod The product that is to be removed. Should not be null
	 * 		and should have previously been added.
	 * @return Returns true if the item was successfully removed and 
	 * 		false if it was not removed.
	 */
	public boolean deleteProduct(Product prod)
	{
		assert prod != null;
		assert products.contains(prod) == true;
		//assert productItems.containsKey(prod) == true;
		
		if (products.contains(prod) == false) return false;
		
		barcodeToProduct.remove(prod.getBarCode());
		
		Set<ProductContainer> containers = productToContainers.get(prod);
		Iterator it = containers.iterator();
		
		while (it.hasNext())
		{
			Set<Product> product = containerToProducts.get(it.next());
			product.remove(prod);
		}
		
		productToContainers.remove(prod);
		removedProducts.add(prod);
		
		return products.remove(prod);
		
	}
	
	/**
	 * This checks to ensure that any modify operations will be valid and will
	 * not change the product so it contains anything illegal. This method assumes
	 * that both parameters are non-null.  It also checks to see if the before
	 * product is actually contained in the list of products and that the 
	 * description will be non-empty, and selflife and threemonthsupply will
	 * both be non-negative. 
	 * 
	 * @param before The Product before it is modified.
	 * @param after The product after it is modified.
	 * @return Returns true if this is a valid modify operation or false
	 * 		if it is not valid.
	 */
	public boolean canEditProduct(Product before, Product after)
	{
		assert before != null;
		assert after != null;
		
		Product toModify = barcodeToProduct.get(before.getBarCode());
		
		if (toModify == null) return false;
		
		if (before.getBarCode() != after.getBarCode()) return false;
		
		if (after.getDescription().length() == 0) return false;
		
		if (after.getShelfLife() < 0) return false;
		
		if (after.getThreeMonthSupply() < 0) return false;
		
		return true;
	}
	
	/**
	 * This method allows a user to modify a specific product. This method 
	 * looks up the item.  If the item is not contained in the internal data
	 * structure then this method throws an IllegalArgumentException.  It also
	 * throws an illegalArgumentException if the barcodes do not match between
	 * the before and after.
	 * 
	 * Otherwise this method will change the existing product to mirror the
	 * parameter after.
	 * 
	 * @param before The product that needs to be changed.
	 * @param after What to change the product to.
	 * @return Returns true if the operation was successful. False otherwise.
	 * @throws IllegalArgumentException IllegalArgumentException is thrown if the
	 * 		'before' product is not contained in the list.
	 */
	public boolean editProduct(Product before, Product after) throws IllegalArgumentException
	{
            //persistence
            TransactionManager.begin();
            ProductDTO pDTO = after.createDTO();
            boolean success = productDAO.edit(pDTO);
            TransactionManager.end(success);
            if(!success)//if false, don't run the rest of the method
            {
                System.out.println("failed to modify DB");
                return false; 
            } 
            
		assert before != null;
		assert after != null;
		
		Product toModify = barcodeToProduct.get(before.getBarCode());
		
		if (toModify == null) throw new IllegalArgumentException();
		
		if (!toModify.getBarCode().equals(after.getBarCode())) 
			throw new IllegalArgumentException();
		
		toModify.setAmount(after.getAmount());
		toModify.setDescription(after.getDescription());
		toModify.setShelfLife(after.getShelfLife());
		/* Fix to update expiration date of all product type */
		CoreFacade.getInstance().getIM().updateExpirationDates(toModify);
		toModify.setThreeMonthSupply(after.getThreeMonthSupply());
				
		return true;
	}
	
	/**
	 * This method creates the acutal product and returns it. This method assumes that
	 * the amount is not null, barcode is not null, description is
	 * not empty, shelflife is >= 0, and threeMonthSupply is >= 0.
	 *
	 * @param amount The amount in the new product.
	 * @param barcode The barcode number of the new product.
	 * @param description The discription of the new product.
	 * @param shelfLife How long this product lasts before needing to be thrown away. 
	 * @param threeMonthSupply The amount of this product needed for 3 months.
	 * @return Returns a newly created Product.
	 * @throws IllegalArgumentException If any arguments are not correct or the instanciation
	 * 		of the new product fails then and IllegalArgument Exception is thrown.
	 */
	public static Product createProduct(Amount amount, Barcode barcode, String description, 
			int shelfLife, int threeMonthSupply) throws IllegalArgumentException
	{
		assert amount != null;
		assert barcode != null;
		assert description != null;
		assert description.length() > 0;
		assert shelfLife >= 0;
		assert threeMonthSupply >= 0;
		
		Product newProduct = new Product(new GregorianCalendar(), barcode, description,
										 amount, shelfLife, threeMonthSupply);
		return newProduct;
	}
	
	/**
	 * This method will find the product that has the corresponding barcode
	 * and returns the product. This method assumes that there is a non-
	 * empty barcode passed into the 'barcode' parameter.
	 * 
	 * @param barcode The barcode of the product to find.
	 * @return Returns the product with that specified name.  IF the product
	 * 		cannot be found this method returns null;
	 */
	public Product getProduct(Barcode barcode)
	{
		assert barcode != null;
		assert barcode.getBarcode().length() > 0;
		
		return barcodeToProduct.get(barcode);
	}
	
	/**
	 * This method will return a List of all the products currently contained
	 * in the system. 
	 * 
	 * @return A List<Product> of all the products in the system.
	 */
	public List<Product> getAllProducts()
	{
		return products;
	}
	
	/**
	 * Returns all of the Product containers that this product is in.  This method
	 * returns a set of all the productContainers that this product is in if the
	 * product exists.  If it doesn't then this method returns null;
	 * @param prod The item to get all containing ProductContainers.
	 * @return Returns a Set of all productContainers that contian this product. Returns
	 * 		null if the product does not exist.
	 */
	public Set<ProductContainer> getAllProductContainers(Product prod)
	{
		assert prod != null;
		
		return productToContainers.get(prod);
	}
	
	/**
	 * This method returns a set of all the different types of products that are 
	 * contained within the ProductContainer 'pc'.  This method will return a
	 * valid Set<Product> if the ProductContainer 'pc' exists, even though that 
	 * Set may be empty.  if the ProductContainer 'pc' does not exist then the
	 * method will return null.
	 * 
	 * @param pc The ProductContainer to get all the products in.
	 * @return A Set of Products if the Container Exists, else it returns null.
	 */
	public Set<Product> getAllProductsInContainer(ProductContainer pc)
	{
		assert pc != null;
		
		Set<Product> prods = containerToProducts.get(pc);
		if(prods == null)
		{
			containerToProducts.put(pc, new HashSet<Product>());
			return containerToProducts.get(pc);
		}
		else
			return  prods;
	}
	
	/**
	 * Adds product container to the necessary data structures
	 * @param pc the product container to add
	 * @throws Exception
	 */
	public void addProductContainer(ProductContainer pc) throws Exception
	{
		assert(pc != null);
		if(pc == null)
			throw new Exception("pc was null");
		if(!containerToProducts.containsKey(pc))
			containerToProducts.put(pc, new HashSet<Product>());
	}
	
	/**
	 * It grabs the all products inside a single storage unit.
	 * @param su the storage unit in question
	 * @return Set of products
	 */
	public Set<Product> getAllProductsFromStorageTree(StorageUnit su)
	{
		assert(su != null);
		
		Set<Product> prods = new HashSet<Product>(getAllProductsInContainer(su));
		List<ProductGroup> pgs = su.getProductGroups();
		for(int i = 0; i < pgs.size(); i++)
		{
			ProductGroup tempPG = pgs.get(i);
			prods.addAll(getAllProductsFromGroupRecursive(tempPG));
			/*
			prods.addAll(getAllProductsInContainer(tempPG));
			while(tempPG.getProductGroups().size() > 0)
			{
				tempPG = tempPG.getProductGroups();
			}
			*/
		}
		return prods;
	}
	/**
	 * Gets all products from successive layer in the container
	 * @param pg productGroup in question
	 * @return Set of products
	 */
	private Set<Product> getAllProductsFromGroupRecursive(ProductGroup pg)
	{
		assert(pg!=null);
		Set<Product> prods = new HashSet<Product>(getAllProductsInContainer(pg));
		List<ProductGroup> groups = pg.getProductGroups();
		for(ProductGroup group : groups)
		{
			prods.addAll(getAllProductsFromGroupRecursive(group));
		}
		return prods;
	}
	
	/**
	 * This method will find which container, if any, within the subtree
	 * of the StorageUnit 'su' the Product 'toCheck' is contained in. This search is
	 * inclusive of 'su'.  The product can only be contained once within a StorageUnit.
	 * This means that the product can be contained in StorageUnit itself or in one
	 * of the ProductGroups within it.  This method returns which ProductContainer the
	 * Product is in.  
	 * 
	 * If the Product is contained in one of the containers within the subtree of 'su'
	 * then this method will return the ProductContainer that the Product is contained
	 * in. If the product is not found in the subtree then this method will return null.
	 * 
	 * @param su The StorageUnit to check for the Product in.
	 * @param toCheck The Product to check for.
	 * @return Returns the ProductContainer the Product is stored in, or null if the
	 * 	Product is not found within the subtree.
	 */
	public ProductContainer getContainer(StorageUnit su, Product toCheck)
	{
		assert su != null;
		assert toCheck != null;
		
		return getContainerRecursive(su,toCheck);
	}
	
	/**
	 * The recursive check for GetContainer().
	 * 
	 * @param su ProductContainer to check in.
	 * @param toCheck The product to check for.
	 * @return Returns the ProductContainer the Product is stored in, or null if the
	 * 	Product is not found within the subtree. 
	 */
	private ProductContainer getContainerRecursive(ProductContainer su, Product toCheck)
	{
		assert su != null;
		assert toCheck != null;
		
		Set<ProductContainer> zContainers = productToContainers.get(toCheck);
		if(zContainers == null)
		{
			System.out.println("WHY IS THIS NULL");
			
			productToContainers.put(toCheck, new HashSet<ProductContainer>());
			zContainers = productToContainers.get(toCheck);
		}
		
		if (zContainers.contains(su))
		{
			return su;
		}
		else
		{
			List<ProductGroup> pgs = su.getProductGroups();
			for (int i = 0; i < pgs.size(); i++)
			{
				ProductContainer pc = getContainerRecursive(pgs.get(i), toCheck);
				if(pc != null)
					return pc;
			}
		}
		
		return null;
	}
	
	/**
	 * Adds a product to the specified container. This assumses the Product is already
	 * inserted into the list of current products for the system. If the product is
	 * not created then this method throws an illegalArguementException.  After this
	 * method is run the ProductManager will contain a maping from the product to the
	 * containter you passed in. It will aslo contain a mapping from the continaer you
	 * passed in to the product.
	 * 
	 * @param toAdd The product to add to the container.
	 * @param addIn The ProductContainer to add the product into.
	 * @return Returns true if the method succeded.
	 * @throws IllegalArgumentException Thrown if the product is not already in the 
	 * 		Product managers list of products.
	 */
	public boolean addProductToContainer(Product toAdd, ProductContainer addIn) throws 
			IllegalArgumentException
	{
		System.out.println("Adding " + toAdd.getDescription() 
				+ " to " + addIn.getName());
		assert addIn != null;
		assert toAdd != null;
		
		TransactionManager.begin();
		IProductsDAO dao = CoreFacade.getInstance().getFactory().getProductsDAO();
		IContainerToProductsDAO dao2 = CoreFacade.getInstance().getFactory().getContsToProdsDAO();
		if(addIn instanceof StorageUnit)
		{
			dao2.add(toAdd.createDTO(), ((StorageUnit)addIn).createDTO());
		}
		else
			dao2.add(toAdd.createDTO(), ((ProductGroup)addIn).createDTO());
		TransactionManager.end(true);
		//get the product
		Set<ProductContainer> containers = productToContainers.get(toAdd);
		if (containers == null) throw new IllegalArgumentException();
		
		containers.add(addIn);
		
		Set<Product> productsIn = containerToProducts.get(addIn);
		
		if (productsIn == null)
		{
			containerToProducts.put(addIn, new HashSet<Product>());
			productsIn = containerToProducts.get(addIn);
		}
		
		productsIn.add(toAdd);
		
		return true;
	}
	
	/**
	 * This method will remove a product from the specified productContainer.  
	 * @param toRemove the Product to remove from the container.
	 * @param removeFrom the container to remove the product from.
	 * @return Returns true if the method succeded.
	 */
	public boolean deleteProductFromContainer(Product toRemove, ProductContainer removeFrom)
	{
		assert toRemove != null;
		assert removeFrom != null;
		
		TransactionManager.begin();
    	IProductsDAO dao = CoreFacade.getInstance().getFactory().getProductsDAO();
    	IContainerToProductsDAO dao2 = CoreFacade.getInstance().getFactory().getContsToProdsDAO();
    	boolean commit = true;// = dao.remove(toRemove.createDTO());
    	if(!commit)
    	{
        	TransactionManager.end(commit);
        	return false;
    	}
    	if(removeFrom instanceof StorageUnit)
    	{
    		commit = dao2.remove(toRemove.createDTO(), ((StorageUnit)removeFrom).createDTO());
    	}
    	else
    		commit = dao2.remove(toRemove.createDTO(), ((ProductGroup)removeFrom).createDTO());
    	
    	TransactionManager.end(commit);
    	if(!commit)
    		return false;
    	
		System.out.println("removing " + toRemove.getDescription() 
				+ " from " + removeFrom.getName());
		
		Set<ProductContainer> containers = productToContainers.get(toRemove);
		if (containers == null) return false;
		containers.remove(removeFrom);
		
		Set<Product> productsIn = containerToProducts.get(removeFrom);
		if(!productsIn.remove(toRemove))
		{
			System.out.println("Error Removing Product:" 
					+ toRemove.getDescription() + " from " + removeFrom.getName());
			return false;
		}
		else
			System.out.println("Successfully Removed Product:" 
					+ toRemove.getDescription() + " from " + removeFrom.getName());
		
		return true;
	}
	
	/**
	 * Remove the ProductContainer from the internal data structures.
	 * 
	 * @param toRemove The productContainer to remove
	 * @return Returns true if the method succeded.
	 */
	public boolean deleteProductContainer(ProductContainer toRemove)
	{
		assert toRemove != null;
		
		Set<Product> productsIn = containerToProducts.get(toRemove);
		
		Iterator it = productsIn.iterator();
		
		while (it.hasNext())
		{
			Set<ProductContainer> containers = productToContainers.get(it.next());
			containers.remove(toRemove);
		}
		
		containerToProducts.remove(toRemove);
		
		return true;
	}
	
	@Override
	public String toString()
	{
		String returnString = "Product Manager:\n\n All Products:\n";
		
		Iterator it = products.listIterator();
		while (it.hasNext())
		{
			Product p = (Product) it.next();
			
			returnString += p.getBarcodeString() + " " + p.getDescription() + "\n";
		}
		
		returnString += "\n Barcode To Products map:\n";
		
		it = barcodeToProduct.keySet().iterator();
		while (it.hasNext())
		{
			Barcode b = (Barcode) it.next();
			
			returnString += b.getBarcode() + " " + barcodeToProduct.get(b).getDescription() + "\n";
		}
		
		returnString += "\n Products to Container map:\n";
		
		it = productToContainers.keySet().iterator();
		while (it.hasNext())
		{
			Product p = (Product) it.next();
			
			returnString += p.getBarcodeString() + " " + p.getDescription() + ":\n";
			it = productToContainers.get(p).iterator();
			while (it.hasNext())
			{
				ProductContainer pc = (ProductContainer) it.next();
				returnString += "   " + pc.getName() + "\n";
			}
			
			returnString += "\n";
		}
		
		return returnString;
	}
     
	
}
